package papi

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"

	"github.com/akamai/AkamaiOPEN-edgegrid-golang/v12/pkg/edgegriderr"
	"github.com/akamai/AkamaiOPEN-edgegrid-golang/v12/pkg/session"
	validation "github.com/go-ozzo/ozzo-validation/v4"
	"github.com/go-ozzo/ozzo-validation/v4/is"
)

type (
	// GetPropertyVersionHostnamesRequest contains parameters required to list property version hostnames
	GetPropertyVersionHostnamesRequest struct {
		PropertyID        string
		PropertyVersion   int
		ContractID        string
		GroupID           string
		ValidateHostnames bool
		IncludeCertStatus bool
	}

	// GetPropertyVersionHostnamesResponse contains all property version hostnames associated to the given parameters
	GetPropertyVersionHostnamesResponse struct {
		AccountID       string                `json:"accountId"`
		ContractID      string                `json:"contractId"`
		GroupID         string                `json:"groupId"`
		PropertyID      string                `json:"propertyId"`
		PropertyVersion int                   `json:"propertyVersion"`
		Etag            string                `json:"etag"`
		Hostnames       HostnameResponseItems `json:"hostnames"`
	}

	// HostnameResponseItems contains the response body for GetPropertyVersionHostnamesResponse
	HostnameResponseItems struct {
		Items []Hostname `json:"items"`
	}

	// Hostname contains information about each of the HostnameResponseItems
	Hostname struct {
		// CnameType is only one supported `EDGE_HOSTNAME` value.
		CnameType HostnameCnameType `json:"cnameType"`

		// EdgeHostnameID identifies each edge hostname.
		EdgeHostnameID string `json:"edgeHostnameId,omitempty"`

		// CnameFrom is the hostname that your end users see, indicated by the `Host` header in end user requests.
		CnameFrom string `json:"cnameFrom"`

		// CnameTo is the edge hostname you point the property hostname to so that you can start serving traffic through Akamai servers. This member corresponds to the edge hostname object's `edgeHostnameDomain` member.
		CnameTo string `json:"cnameTo,omitempty"`

		// CertProvisioningType indicates the certificate's provisioning type. Either `CPS_MANAGED` type for the certificates you create with the Certificate Provisioning System API (CPS), `DEFAULT` for the Default Domain Validation (DV) certificates created automatically, or `CCM` type for the third party certificates you create with the Cloud Certificate Manager.
		CertProvisioningType string `json:"certProvisioningType"`

		// CertStatus determines whether a hostname is capable of serving secure content over the staging or production network.
		CertStatus CertStatusItem `json:"certStatus,omitempty"`

		// CCMCertStatus is deployment status for the RSA and ECDSA certificates created with Cloud Certificate Manager (CCM).
		CCMCertStatus *CCMCertStatus `json:"ccmCertStatus,omitempty"`

		// CCMCertificates is certificate identifiers and links for the CCM-managed certificates.
		CCMCertificates *CCMCertificates `json:"ccmCertificates,omitempty"`

		// MTLS is mutual TLS configuration settings applicable to the Cloud Certificate Manager (CCM) hostnames.
		MTLS *MTLS `json:"mtls,omitempty"`

		// TLSConfiguration is optional TLS configuration settings applicable to the Cloud Certificate Manager (CCM) hostnames.
		TLSConfiguration *TLSConfiguration `json:"tlsConfiguration,omitempty"`
	}

	// CCMCertStatus is status of CCM certificates in each environment.
	CCMCertStatus struct {
		// ECDSAProductionStatus is ECDSA production status.
		ECDSAProductionStatus string `json:"ecdsaProductionStatus,omitempty"`

		// ECDSAStagingStatus is ECDSA staging status.
		ECDSAStagingStatus string `json:"ecdsaStagingStatus,omitempty"`

		// RSAProductionStatus is RSA production status.
		RSAProductionStatus string `json:"rsaProductionStatus,omitempty"`

		// RSAStagingStatus is RSA staging status.
		RSAStagingStatus string `json:"rsaStagingStatus,omitempty"`
	}

	// CertStatusItem contains information about certificate status for specific Hostname.
	CertStatusItem struct {
		// ValidationCname is the CNAME record used to validate the certificate’s domain.
		ValidationCname ValidationCname `json:"validationCname,omitempty"`

		// Staging contains certificate status for the hostname on the staging network.
		Staging []StatusItem `json:"staging,omitempty"`

		// Production contains certificate status for the hostname on the production network.
		Production []StatusItem `json:"production,omitempty"`
	}

	// CCMCertificates contains identifiers for the RSA and ECDSA certificates.
	CCMCertificates struct {
		// ECDSACertID is certificate ID for ECDSA.
		ECDSACertID string `json:"ecdsaCertId,omitempty"`

		// ECDSACertLink is link to the ECSDA certificate.
		ECDSACertLink string `json:"ecdsaCertLink,omitempty"`

		// RSACertID is certificate ID for RSA.
		RSACertID string `json:"rsaCertId,omitempty"`

		// RSACertLink is link to the RSA certificate.
		RSACertLink string `json:"rsaCertLink,omitempty"`
	}

	// MTLS is mutual TLS configuration used only for the `CCM` provisioning.
	MTLS struct {
		// CASetID is ID of the Client CA set used for mutual TLS.
		CASetID string `json:"caSetId,omitempty"`

		// CASetLink is link of the Client CA set used for mutual TLS.
		CASetLink string `json:"caSetLink,omitempty"`

		// CheckClientOCSP specifies whether to check the OCSP status of the client certificate.
		CheckClientOCSP bool `json:"checkClientOcsp,omitempty"`

		// SendCASetClient specifies whether to send the CA set to the client during the TLS handshake.
		SendCASetClient bool `json:"sendCaSetClient,omitempty"`
	}

	// TLSConfiguration is optional TLS configuration settings applicable to the Cloud Certificate Manager (CCM) hostnames.
	TLSConfiguration struct {
		// CipherProfile is cipher profile name.
		CipherProfile string `json:"cipherProfile,omitempty"`

		// DisallowedTLSVersions is  list of TLS versions that are disallowed.
		DisallowedTLSVersions []string `json:"disallowedTlsVersions,omitempty"`

		// StapleServerOcspResponse specifies whether to staple the OCSP response for the server certificate.
		StapleServerOcspResponse bool `json:"stapleServerOcspResponse,omitempty"`

		// FIPSMode specifies whether to enable the FIPS mode.
		FIPSMode bool `json:"fipsMode,omitempty"`
	}

	// ValidationCname is the CNAME record used to validate the certificate’s domain
	ValidationCname struct {
		Hostname string `json:"hostname,omitempty"`
		Target   string `json:"target,omitempty"`
	}

	// StatusItem determines whether a hostname is capable of serving secure content over the staging or production network.
	StatusItem struct {
		Status string `json:"status,omitempty"`
	}

	// UpdatePropertyVersionHostnamesRequest contains parameters required to update the set of hostname entries for a property version
	UpdatePropertyVersionHostnamesRequest struct {
		PropertyID        string
		PropertyVersion   int
		ContractID        string
		GroupID           string
		ValidateHostnames bool
		IncludeCertStatus bool
		Hostnames         []Hostname
	}

	// UpdatePropertyVersionHostnamesResponse contains information about each of the HostnameRequestItems
	UpdatePropertyVersionHostnamesResponse struct {
		AccountID       string                `json:"accountId"`
		ContractID      string                `json:"contractId"`
		GroupID         string                `json:"groupId"`
		PropertyID      string                `json:"propertyId"`
		PropertyVersion int                   `json:"propertyVersion"`
		Etag            string                `json:"etag"`
		Hostnames       HostnameResponseItems `json:"hostnames"`
	}

	// PatchPropertyVersionHostnamesRequest contains parameters for patch property version hostnames
	PatchPropertyVersionHostnamesRequest struct {
		PropertyID        string
		PropertyVersion   int
		ContractID        string
		GroupID           string
		ValidateHostnames bool
		IncludeCertStatus bool
		Body              PatchPropertyVersionHostnamesRequestBody
	}

	// PatchPropertyVersionHostnamesRequestBody contains the request body for patching property version hostnames
	PatchPropertyVersionHostnamesRequestBody struct {
		Add    []HostnameAdd `json:"add,omitempty"`
		Remove []string      `json:"remove,omitempty"`
	}

	// HostnameAdd contains information about the hostname to be added in patch property version hostnames
	HostnameAdd struct {
		// CnameFrom is the hostname that your end users see, indicated by the `Host` header in end user requests.
		CnameFrom string `json:"cnameFrom"`

		// CnameType is only one supported `EDGE_HOSTNAME` value.
		CnameType HostnameCnameType `json:"cnameType,omitempty"`

		// CnameTo is the edge hostname you point the property hostname to so that you can start serving traffic through Akamai servers. This member corresponds to the edge hostname object's `edgeHostnameDomain` member.
		CnameTo string `json:"cnameTo,omitempty"`

		// CertProvisioningType indicates the certificate's provisioning type. Either `CPS_MANAGED` type for the certificates you create with the Certificate Provisioning System API (CPS), `DEFAULT` for the Default Domain Validation (DV) certificates created automatically, or `CCM` type for the third party certificates you create with the Cloud Certificate Manager.
		CertProvisioningType CertType `json:"certProvisioningType,omitempty"`

		// EdgeHostnameID identifies each edge hostname.
		EdgeHostnameID string `json:"edgeHostnameId,omitempty"`

		// MTLS is mutual TLS configuration settings applicable to the Cloud Certificate Manager (CCM) hostnames.
		MTLS *MTLS `json:"mtls,omitempty"`

		// TLSConfiguration is optional TLS configuration settings applicable to the Cloud Certificate Manager (CCM) hostnames.
		TLSConfiguration *TLSConfiguration `json:"tlsConfiguration,omitempty"`

		// CCMCertStatus is deployment status for the RSA and ECDSA certificates created with Cloud Certificate Manager (CCM).
		CCMCertificates *CCMCertificates `json:"ccmCertificates,omitempty"`
	}

	// PatchPropertyVersionHostnamesResponse contains response from patch property version hostnames
	PatchPropertyVersionHostnamesResponse struct {
		AccountID       string                `json:"accountId"`
		ContractID      string                `json:"contractId"`
		GroupID         string                `json:"groupId"`
		Etag            string                `json:"etag"`
		PropertyID      string                `json:"propertyId"`
		PropertyName    string                `json:"propertyName"`
		PropertyVersion int                   `json:"propertyVersion"`
		Hostnames       HostnameResponseItems `json:"hostnames"`
	}

	// HostnameCnameType represents HostnameCnameType enum
	HostnameCnameType string
)

const (
	// HostnameCnameTypeEdgeHostname const
	HostnameCnameTypeEdgeHostname HostnameCnameType = "EDGE_HOSTNAME"
)

// Validate validates HostnameCnameType.
func (t HostnameCnameType) Validate() validation.InRule {
	return validation.In(HostnameCnameTypeEdgeHostname).
		Error(fmt.Sprintf("value '%s' is invalid. There is only one supported value of: %s",
			t, HostnameCnameTypeEdgeHostname))
}

// Validate validates GetPropertyVersionHostnamesRequest
func (ph GetPropertyVersionHostnamesRequest) Validate() error {
	return validation.Errors{
		"PropertyID":      validation.Validate(ph.PropertyID, validation.Required),
		"PropertyVersion": validation.Validate(ph.PropertyVersion, validation.Required),
	}.Filter()
}

// Validate validates UpdatePropertyVersionHostnamesRequest
func (r UpdatePropertyVersionHostnamesRequest) Validate() error {
	return edgegriderr.ParseValidationErrors(validation.Errors{
		"PropertyID":      validation.Validate(r.PropertyID, validation.Required),
		"PropertyVersion": validation.Validate(r.PropertyVersion, validation.Required),
		"Hostnames":       validation.Validate(r.Hostnames),
	})
}

// Validate validates Hostname
func (h Hostname) Validate() error {
	return validation.Errors{
		"MTLS":                validation.Validate(h.MTLS),
		"CCMCertificates":     validation.Validate(h.CCMCertificates),
		"ValidateCCMHostname": validateCCMHostname(h.CertProvisioningType, h.CCMCertificates, h.MTLS, h.TLSConfiguration),
	}.Filter()
}

// Validate validates MTLS
func (m MTLS) Validate() error {
	return validation.Errors{
		"CASetID": validation.Validate(m.CASetID, validation.Required, is.Digit),
	}.Filter()
}

// Validate validates CCMCertificates
func (c CCMCertificates) Validate() error {
	return validation.Errors{
		"RSACertID":   validation.Validate(c.RSACertID, validation.When(c.RSACertID != "", is.Digit)),
		"ECDSACertID": validation.Validate(c.ECDSACertID, validation.When(c.ECDSACertID != "", is.Digit)),
	}.Filter()
}

func validateCCMHostname(certType string, certs *CCMCertificates, mTLS *MTLS, tls *TLSConfiguration) error {
	if certType != string(CertTypeCCM) {
		if certs != nil {
			return errors.New("the CCM cert details are provided without `certProvisioningType` set to `CCM`")
		}
		if mTLS != nil {
			return errors.New("the mTLS configuration is provided without `certProvisioningType` set to `CCM`")
		}
		if tls != nil {
			return errors.New("the TLS configuration is provided without `certProvisioningType` set to `CCM`")
		}
		return nil
	}
	if certs == nil {
		return errors.New("when using `certProvisioningType` set to `CCM`, the request body must contain `ccmCertificates` with at least `rsaCertId` or `ecdsaCertId`")
	}
	if certs.RSACertID == "" && certs.ECDSACertID == "" {
		return errors.New("either RSACertID or ECDSACertID must be provided")
	}
	if tls != nil && len(tls.CipherProfile) == 0 {
		return errors.New("the cipher profile is empty in the TLS configuration")
	}
	return nil
}

// Validate validates PatchPropertyVersionHostnamesRequest
func (r PatchPropertyVersionHostnamesRequest) Validate() error {
	return edgegriderr.ParseValidationErrors(validation.Errors{
		"PropertyID":      validation.Validate(r.PropertyID, validation.Required),
		"PropertyVersion": validation.Validate(r.PropertyVersion, validation.Required),
		"Body":            validation.Validate(r.Body, validation.Required),
	})
}

// Validate validates PatchPropertyVersionHostnamesRequestBody
func (b PatchPropertyVersionHostnamesRequestBody) Validate() error {
	return validation.Errors{
		"Add": validation.Validate(b.Add),
	}.Filter()
}

// Validate validates HostnameAdd
func (h HostnameAdd) Validate() error {
	return validation.Errors{
		"CnameFrom": validation.Validate(h.CnameFrom, validation.Required),
		"CnameType": validation.Validate(h.CnameType,
			validation.When(h.CnameType != "", h.CnameType.Validate())),
		"CertProvisioningType": validation.Validate(h.CertProvisioningType,
			validation.When(h.CertProvisioningType != "", h.CertProvisioningType.Validate())),
		"MTLS":                validation.Validate(h.MTLS),
		"CCMCertificates":     validation.Validate(h.CCMCertificates),
		"ValidateCCMHostname": validateCCMHostname(string(h.CertProvisioningType), h.CCMCertificates, h.MTLS, h.TLSConfiguration),
		"required parameters": validation.Validate(nil,
			validation.By(func(interface{}) error {
				if h.CnameTo == "" && h.EdgeHostnameID == "" {
					return fmt.Errorf("either CnameTo or EdgeHostnameID must be provided")
				}
				return nil
			})),
	}.Filter()
}

var (
	// ErrGetPropertyVersionHostnames represents error when fetching hostnames fails
	ErrGetPropertyVersionHostnames = errors.New("fetching hostnames")
	// ErrUpdatePropertyVersionHostnames represents error when updating hostnames fails
	ErrUpdatePropertyVersionHostnames = errors.New("updating hostnames")
	// ErrPatchPropertyVersionHostnames represents error when patching hostnames fails
	ErrPatchPropertyVersionHostnames = errors.New("patching hostnames")
)

func (p *papi) GetPropertyVersionHostnames(ctx context.Context, params GetPropertyVersionHostnamesRequest) (*GetPropertyVersionHostnamesResponse, error) {
	logger := p.Log(ctx)
	logger.Debug("GetPropertyVersionHostnames")

	if err := params.Validate(); err != nil {
		return nil, fmt.Errorf("%s: %w: %s", ErrGetPropertyVersionHostnames, ErrStructValidation, err)
	}

	uri, err := url.Parse(fmt.Sprintf("/papi/v1/properties/%s/versions/%d/hostnames", params.PropertyID, params.PropertyVersion))
	if err != nil {
		return nil, fmt.Errorf("%w: failed to parse url: %s", ErrGetPropertyVersionHostnames, err)
	}
	q := url.Values{}
	if params.GroupID != "" {
		q.Set("groupId", params.GroupID)
	}
	if params.ContractID != "" {
		q.Set("contractId", params.ContractID)
	}
	q.Set("validateHostnames", strconv.FormatBool(params.ValidateHostnames))
	q.Set("includeCertStatus", strconv.FormatBool(params.IncludeCertStatus))
	uri.RawQuery = q.Encode()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to create request: %s", ErrGetPropertyVersionHostnames, err)
	}

	var hostnames GetPropertyVersionHostnamesResponse
	resp, err := p.Exec(req, &hostnames)
	if err != nil {
		return nil, fmt.Errorf("%w: request failed: %s", ErrGetPropertyVersionHostnames, err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s: %w", ErrGetPropertyVersionHostnames, p.Error(resp))
	}

	return &hostnames, nil
}

func (p *papi) UpdatePropertyVersionHostnames(ctx context.Context, params UpdatePropertyVersionHostnamesRequest) (*UpdatePropertyVersionHostnamesResponse, error) {
	logger := p.Log(ctx)
	logger.Debug("UpdatePropertyVersionHostnames")

	if err := params.Validate(); err != nil {
		return nil, fmt.Errorf("%s: %w: %s", ErrUpdatePropertyVersionHostnames, ErrStructValidation, err)
	}

	uri, err := url.Parse(fmt.Sprintf("/papi/v1/properties/%s/versions/%v/hostnames", params.PropertyID, params.PropertyVersion))
	if err != nil {
		return nil, fmt.Errorf("%w: failed to parse url: %s", ErrUpdatePropertyVersionHostnames, err)
	}

	q := url.Values{}
	if params.GroupID != "" {
		q.Set("groupId", params.GroupID)
	}
	if params.ContractID != "" {
		q.Set("contractId", params.ContractID)
	}
	q.Set("validateHostnames", strconv.FormatBool(params.ValidateHostnames))
	q.Set("includeCertStatus", strconv.FormatBool(params.IncludeCertStatus))
	uri.RawQuery = q.Encode()

	req, err := http.NewRequestWithContext(ctx, http.MethodPut, uri.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to create request: %s", ErrUpdatePropertyVersionHostnames, err)
	}

	var hostnames UpdatePropertyVersionHostnamesResponse
	newHostnames := params.Hostnames
	if newHostnames == nil {
		newHostnames = []Hostname{}
	}
	resp, err := p.Exec(req, &hostnames, newHostnames)
	if err != nil {
		return nil, fmt.Errorf("%w: request failed: %s", ErrUpdatePropertyVersionHostnames, err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s: %w", ErrUpdatePropertyVersionHostnames, p.Error(resp))
	}

	return &hostnames, nil
}

func (p *papi) PatchPropertyVersionHostnames(ctx context.Context, params PatchPropertyVersionHostnamesRequest) (*PatchPropertyVersionHostnamesResponse, error) {
	logger := p.Log(ctx)
	logger.Debug("PatchPropertyVersionHostnames")

	if err := params.Validate(); err != nil {
		return nil, fmt.Errorf("%w: %w: %w", ErrPatchPropertyVersionHostnames, ErrStructValidation, err)
	}

	query := url.Values{}
	if params.ContractID != "" {
		query.Set("contractId", params.ContractID)
	}
	if params.GroupID != "" {
		query.Set("groupId", params.GroupID)
	}
	if params.ValidateHostnames {
		query.Set("validateHostnames", fmt.Sprintf("%t", params.ValidateHostnames))
	}
	if params.IncludeCertStatus {
		query.Set("includeCertStatus", fmt.Sprintf("%t", params.IncludeCertStatus))
	}

	uri, err := url.Parse(fmt.Sprintf(
		"/papi/v1/properties/%s/versions/%v/hostnames",
		params.PropertyID,
		params.PropertyVersion,
	))
	if err != nil {
		return nil, fmt.Errorf("%w: failed to parse url: %w", ErrPatchPropertyVersionHostnames, err)
	}
	uri.RawQuery = query.Encode()

	req, err := http.NewRequestWithContext(ctx, http.MethodPatch, uri.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to create request: %w", ErrPatchPropertyVersionHostnames, err)
	}

	var result PatchPropertyVersionHostnamesResponse
	resp, err := p.Exec(req, &result, params.Body)
	if err != nil {
		return nil, fmt.Errorf("%w: request failed: %w", ErrPatchPropertyVersionHostnames, err)
	}
	defer session.CloseResponseBody(resp)

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%w: %w", ErrPatchPropertyVersionHostnames, p.Error(resp))
	}

	return &result, nil
}
